#pragma once

#include "basicDevice.hpp"
#include "devUpgradeGuard.hpp"
#include "fileGuard.hpp"
#include "modbus.hpp"
#include "point.hpp"
#include "yaml-cpp/yaml.h"
#include <filesystem>

struct ModbusDevice_t : public Device_t {
    inline static std::atomic<bool> isUpgrade = false;

    using OnlineStateChange_t = std::function<void(void *)>;

    PointTable_t pointTable;
    std::string profile;
    std::uint8_t slaveAddr;
    std::string mac;
    ModbusRtu *rtu;

    ModbusDevice_t &setModbusRtu(ModbusRtu *r);

    ModbusDevice_t &setMac(const std::string &mac);
    ModbusDevice_t &setMac(const uint8_t *mac, std::size_t len);

    ModbusDevice_t &setModel(const uint8_t *model, std::size_t len);

    ModbusDevice_t &setSlaveAddr(uint8_t addr);

    static bool hasDeviceUpgrade(void);

    bool readPoint(const std::string &name, bool enableOffline = false);
    bool writePoint(const std::string &name, const std::vector<uint8_t> &data);
    bool writePoint(const std::string &name, uint32_t value, bool enableOffline = false);

    std::vector<OnlineStateChange_t> onLineStateChangeCallback;

    void registerOnlineStateChangeCallback(const OnlineStateChange_t &f);

    void online(void);
    void offline(void);
    bool isOnline(void);
    bool isOffline(void);

    ModbusDevice_t();
    ModbusDevice_t(const ModbusDevice_t &) = default;

    bool fromYaml(void);

    bool parsePoint(const YAML::detail::iterator_value &point, PointTable_t &PointTable);

    bool upgrade(const std::string_view &fileName);

    // bool toYaml(void);
};

ModbusDevice_t::ModbusDevice_t()
{
    _isOnline = DeviceStat_t::DEVICE_INIT;
    slaveAddr = 0;
    rtu = nullptr;
}

ModbusDevice_t &ModbusDevice_t::setModbusRtu(ModbusRtu *r)
{
    rtu = r;
    return *this;
}

ModbusDevice_t &ModbusDevice_t::setMac(const std::string &mac)
{
    this->mac = mac;
    return *this;
}

ModbusDevice_t &ModbusDevice_t::setMac(const uint8_t *mac, std::size_t len)
{
    std::stringstream ss;
    for (int i = 0; i < len; i++) {
        ss << std::hex << std::setw(2) << std::setfill('0') << std::uppercase << U32(mac[i]);
        if (i != len - 1) {
            ss << ":";
        }
    }

    this->mac = ss.str();
    return *this;
}

ModbusDevice_t &ModbusDevice_t::setModel(const uint8_t *model, std::size_t len)
{
    std::stringstream ss;
    for (int i = 0; i < len; i++) {
        ss << std::hex << std::setw(2) << std::setfill('0') << U32(model[i]);
    }

    this->model = ss.str();
    return *this;
}

ModbusDevice_t &ModbusDevice_t::setSlaveAddr(uint8_t addr)
{
    slaveAddr = addr;
    return *this;
}

bool ModbusDevice_t::hasDeviceUpgrade(void)
{
    return isUpgrade.load();
}

void ModbusDevice_t::registerOnlineStateChangeCallback(const OnlineStateChange_t &f)
{
    onLineStateChangeCallback.emplace_back(f);
}

void ModbusDevice_t::online(void)
{
    if (_isOnline != DeviceStat_t::DEVICE_ONLINE) {
        _isOnline = DeviceStat_t::DEVICE_ONLINE;
        std::for_each(onLineStateChangeCallback.begin(), onLineStateChangeCallback.end(), [this](const OnlineStateChange_t &f) {if (f) {f((void *)this);} });
    }

    _isOnline = DeviceStat_t::DEVICE_ONLINE;
}

void ModbusDevice_t::offline(void)
{
    if (_isOnline != DeviceStat_t::DEVICE_OFFLINE) {
        _isOnline = DeviceStat_t::DEVICE_OFFLINE;
        std::for_each(onLineStateChangeCallback.begin(), onLineStateChangeCallback.end(), [this](const OnlineStateChange_t &f) {if (f) {f((void *)this);} });
    }

    _isOnline = DeviceStat_t::DEVICE_OFFLINE;
}

bool ModbusDevice_t::isOnline(void)
{
    return _isOnline == DeviceStat_t::DEVICE_ONLINE;
}

bool ModbusDevice_t::isOffline(void)
{
    return !isOnline();
}

bool ModbusDevice_t::readPoint(const std::string &name, bool enableOffline)
{
    if (hasDeviceUpgrade()) {
        modbus_error("current has device upgrade, please wait a little time");
        return false;
    }

    if (!enableOffline) {
        if (isOffline()) {
            modbus_error("device not online, please check!!! id --> {}, modbus addr --> {}", id.value(), slaveAddr);
            return false;
        }
    }

    if (!rtu) {
        modbus_error("not rtu handler, please check!!!");
        return false;
    }

    auto point = pointTable.findReadPoint(name);
    if (!point.has_value()) {
        modbus_error("this point table not have this point, point name --> {}, device id --> {} please check!!!", name, id.value());
        return false;
    }

    Point_t *p = point.value();

    if (!p->readFunc) {
        modbus_error("point read function code is {} vaild, point name --> {}, device id --> {} please check!!!", p->readFunc, name, id.value());
        p->opFailed();
        return false;
    }

    Data data;

    if (p->readFunc == MODBUS_FC_READ_HOLDING_REGISTERS) {
        data = rtu->readRegisters(slaveAddr, p->addr, p->len, hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits);
    } else if (p->readFunc == MODBUS_FC_REPORT_SLAVE_ID) {
        data = rtu->reportId(slaveAddr, hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits);
    } else {
        modbus_error("not suppost read function code {}, device id --> {}, please check!!!", p->readFunc, id.value());
        p->opFailed();
        return false;
    }

    if (data.empty()) {
        if (errno == ETIMEDOUT) {
            offline();
        }
        modbus_error("can't recv some data point name --> {}, device id --> {}, --> errno {} ", name, id.has_value() ? id.value() : 0, errno);
        p->opFailed();
        return false;
    }

    online();
    p->time = std::chrono::high_resolution_clock::now();
    p->opSuccess();

    /* 如果是父点 */
    if (p->isFatherPoint()) {
        for (auto &_point : *p->pointTable) {
            if (_point.second.offset.has_value()) {
                NewData_t newData {};
                if (_point.second.type == TYPE_BITS) {
                    newData = data.getPartBits<uint32_t>(_point.second.offset->first, _point.second.offset->second);
                } else {
                    newData = data.subPartByte(_point.second.offset->first / 8, _point.second.offset->second / 8);
                }

                if (_point.second.pointOperatorCallback.has_value()) {
                    (*_point.second.pointOperatorCallback)((void *)this, _point.first, _point.second.rData, newData);
                }
                _point.second.rData = newData;
                _point.second.time = std::chrono::high_resolution_clock::now();
                _point.second.opSuccess();
            }
        }
    }

    if (p->type != TYPE_CLASS && p->pointOperatorCallback.has_value()) {
        (*p->pointOperatorCallback)((void *)this, name, p->rData, data);
    }

    p->rData = data;

    return true;
}

bool ModbusDevice_t::writePoint(const std::string &name, uint32_t value, bool enableOffline)
{
    if (hasDeviceUpgrade()) {
        modbus_error("current has device upgrade, please wait a little time");
        return false;
    }

    if (!enableOffline) {
        if (isOffline()) {
            modbus_error("device not online, please check!!! id --> {}, modbus addr --> {}", id.value(), slaveAddr);
            return false;
        }
    }

    auto point = pointTable.findPoint(name);
    if (!point.has_value()) {
        modbus_error("this point table not have this point, please check!!!");
        return false;
    }
    modbus_info("name --> {}, len --> {}", name, point.value()->len);
    if (point.value()->len == 1) {
        return writePoint(name, {U8(value >> 8), U8(value)});
    } else if (point.value()->len == 2) {
        return writePoint(name, {U8(value >> 24), U8(value >> 16), U8(value >> 8), U8(value)});
    }

    modbus_error("write point failed and point name is {}, len is {}, len not is 1 or 2", name, point.value()->len);

    return false;
}

bool ModbusDevice_t::writePoint(const std::string &name, const std::vector<uint8_t> &data)
{
    if (hasDeviceUpgrade()) {
        modbus_error("current has device upgrade, please wait a little time");
        return false;
    }

    if (!rtu) {
        modbus_error("not rtu handler, please check!!!");
        return false;
    }

    auto point = pointTable.findPoint(name);
    if (!point.has_value()) {
        modbus_error("this point table not have this point, please check!!!");
        return false;
    }

    Point_t *p = point.value();

    if (!p->writeFunc) {
        modbus_error("point write function code is {} vaild, please check!!!", p->writeFunc);
        p->opFailed();
        return false;
    }

    Data wdata;

    if (p->writeFunc == MODBUS_FC_WRITE_SINGLE_REGISTER) {
        if (p->len != 1) {
            modbus_error("point register len is {} but function code is {}, please check point table!!!", p->len, p->writeFunc);
            p->opFailed();
            return false;
        }

        rtu->writeRegister(slaveAddr, p->addr, Data(data).get<uint16_t>(), hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits) ? p->opSuccess() : p->opFailed();

        if (p->opResult()) {
            if (p->pointOperatorCallback.has_value()) {
                (*p->pointOperatorCallback)((void *)this, name, p->rData, wdata);
            }
            online();
        } else {
            if (errno == ETIMEDOUT) {
                offline();
            }
        }

        return p->opResult();
    } else if (p->writeFunc == MODBUS_FC_WRITE_MULTIPLE_REGISTERS) {
        rtu->writeRegisters(slaveAddr, p->addr, p->len, data, hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits) ? p->opSuccess() : p->opFailed();

        if (p->opResult()) {
            if (p->pointOperatorCallback.has_value()) {
                (*p->pointOperatorCallback)((void *)this, name, p->rData, wdata);
            }
            online();
        } else {
            if (errno == ETIMEDOUT) {
                offline();
            }
        }

        return p->opResult();
    }

    modbus_error("point write function code is {} not suppose, please check!!!", p->writeFunc);
    p->opFailed();

    return false;
}

bool ModbusDevice_t::parsePoint(const YAML::detail::iterator_value &point, PointTable_t &PointTable)
{
    std::string pointName {};
    Point_t p;

    if (!point["name"].IsDefined()) {
        modbus_error("point not has name, please check!!!");
        throw std::runtime_error("point not has name, please check!!!");
        return false;
    }
    pointName = point["name"].as<std::string>();

    if (!point["type"].IsDefined()) {
        modbus_error("point not has type, please check!!!");
        throw std::runtime_error("point name -> " + pointName + " point not has type, please check!!!");
        return false;
    }
    p.type = point["type"].as<std::string>();

    if (!point["len"].IsDefined()) {
        modbus_error("point not has len, please check!!!");
        throw std::runtime_error("point name -> " + pointName + " point not has len, please check!!!");
        return false;
    }
    p.len = point["len"].as<uint16_t>();

    if (!point["addr"].IsDefined()) {
        modbus_error("point not has addr, please check!!!");
        throw std::runtime_error("point name -> " + pointName + " point not has addr, please check!!!");
        return false;
    }
    p.addr = point["addr"].as<uint16_t>();

    if (!point["funCode"].IsDefined()) {
        modbus_error("point name -> " + pointName + " point not has funCode, please check lower or upper!!!");
        throw std::runtime_error("point not has addr, please check!!!");
        return false;
    }

    if (!point["funCode"].IsSequence()) {
        modbus_error("point funCode not is sequence, please check profile!!!");
        throw std::runtime_error(std::string("point name --> ") + pointName + " point funCode not is sequence, please check profile!!!");
        return false;
    }

    if (point["funCode"].size() != 2) {
        modbus_error("point funCode size not is 2, please check profile!!!");
        throw std::runtime_error("point funCode size not is 2, please check profile!!!");
        return false;
    }

    if (!point["funCode"][0].IsNull()) {
        p.readFunc = point["funCode"][0].as<uint16_t>();
    }
    if (!point["funCode"][1].IsNull()) {
        p.writeFunc = point["funCode"][1].as<uint16_t>();
    }

    if (point["precision"].IsDefined()) {
        p.unit = point["precision"].as<double>();
    }

    if (point["offset"].IsDefined()) {
        if (point["offset"].IsSequence()) {
            if (point["offset"].size() == 2) {
                p.offset = std::make_pair(point["offset"][0].as<uint32_t>(), point["offset"][1].as<uint32_t>());
            }
        }
    }

    if (point["subItems"].IsDefined()) {
        p.pointTable = std::make_shared<PointTable_t>();
        for (const auto &item : point["subItems"]) {
            parsePoint(item, *p.pointTable);
        }
    }

    PointTable.insert(std::make_pair(pointName, p));
    return true;
}

bool ModbusDevice_t::fromYaml(void)
{
    try {
        YAML::Node root = YAML::LoadFile(profile);

        if (!root["resources"].IsDefined()) {
            modbus_error("can't find resources from {}", profile);
            return false;
        }

        if (!root["resources"].IsSequence()) {
            modbus_error("{} resources not is sequense, please check!!!", profile);
            return false;
        }

        for (const auto &point : root["resources"]) {
            parsePoint(point, pointTable);
        }
    }
    catch (const std::exception &e) {
        modbus_error("parse yaml file catch exception {}, yaml file --> {}", e.what(), profile);
    }

    return true;
}

bool ModbusDevice_t::upgrade(const std::string_view &fileName)
{
    if (!std::filesystem::exists(fileName)) {
        modbus_error("ota file not exist, file name --> {}, device id --> {}", fileName, id.value());
        return false;
    }

    DevUpgradeGuard upgradeGuard(ModbusDevice_t::isUpgrade);

    auto ret = rtu->writeRegisters(slaveAddr, 0x00, 0x01, std::vector<uint16_t> {0x0008}, hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits);
    if (!ret) {
        modbus_error("start device upgrade failed, file name --> {}, device id --> {}", fileName, id.value());
        return false;
    }

    uint16_t upgradeRegAddr = 0xf000, packageNum = 0;
    uint32_t fileLen = 0, fileCrc = 0, remainLen = std::filesystem::file_size(fileName);
    constexpr uint32_t size = 128;
    auto buffer = std::make_unique<uint8_t[]>(size);
    int fd = open(fileName.data(), O_RDONLY);
    if (fd < 0) {
        modbus_error("can't open ota file, file name --> {}, device id --> {}", fileName, id.value());
        return false;
    }

    FileGuard fileGuard(fd);

    do {
        auto recv = read(fd, buffer.get(), std::min(size, remainLen));
        if (recv < 0) {
            modbus_error("read file failed error --> {}", strerror(errno));
            return false;
        } else if (!recv) {
            modbus_info("read file end --> eof");
            break;
        }

        if (!packageNum) {
            fileLen = U8_TO_U32(buffer[15], buffer[14], buffer[13], buffer[12]);
            modbus_info("file len --> {}", fileLen);
            fileCrc = U8_TO_U32(buffer[16], buffer[17], buffer[18], buffer[19]);
            modbus_info("file crc --> {}", fileCrc);
        } else {
            ret = rtu->writeRegisters(slaveAddr, upgradeRegAddr, recv / 2, std::vector<uint8_t>(buffer.get(), buffer.get() + recv), hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits);
            if (!ret) {
                modbus_error("device upgrade failed transmit failed, filename --> {}, device id --> {}", fileName, id.value());
                return false;
            }
            ++upgradeRegAddr;
        }
        ++packageNum;
        remainLen -= recv;
    } while (remainLen > 0);

    auto stopPack = std::vector<uint8_t> {0x00, 0x0a, 0x00, 0x00, 0xff, 0xff, U8(fileLen >> 24 & 0xff), U8(fileLen >> 16 & 0xff), U8(fileLen >> 8 & 0xff), U8(fileLen & 0xff), U8(fileCrc >> 24 & 0xff), U8(fileCrc >> 16 & 0xff), U8(fileCrc >> 8 & 0xff), U8(fileCrc & 0xff)};
    ret = rtu->writeRegisters(slaveAddr, 0x00, 0x07, stopPack, hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits);
    if (!ret) {
        modbus_error("device upgrade stop failed, fileName --> {}, device id --> {}", fileName, id.value());
        return false;
    }

    ret = rtu->writeRegisters(slaveAddr, 0x00, 0x01, std::vector<uint16_t> {0x0009}, hardware.port, hardware.baudrate, hardware.parity, hardware.databits, hardware.stopbits);
    if (!ret) {
        modbus_error("device reboot failed, file name --> {}, device id --> {}", fileName, id.value());
        return false;
    }

    return true;
}